"""
This module translates conceptual compiler inputs into command lines for
various compiler chains.
"""

import sys
import os
import re
import platform
import itertools
import struct
import subprocess
import ctypes

from made.util import enum, command, misc
from made import runtime

language = enum.Enumeration("c", "cpp")
operation = enum.Enumeration(
    "compile", "link_static", "link_dynamic", "link_binary",
    "make_dependencies")

class CPPCompilerChain(object):
    name = None

    def __init__(self):
        self._find_executables()

    def compile_files(
            self, source_and_object_files, defines, include_dirs, is_debug,
            is_for_dynamic, extra_options_dict={},
            overridden_debug_options=None,
            overridden_optimization_options=None):
        raise NotImplementedError()

    def link_static_library(self, object_files, output_library):
        raise NotImplementedError()

    def link_dynamic_library(
            self, object_files, output_library, link_dirs, link_libraries,
            is_debug, import_lib=None, linker_language=language.cpp,
            use_rpath=True, version=None, default_libs=()):
        raise NotImplementedError()

    def link_binary(
            self, object_files, output_binary, link_dirs, link_libraries,
            is_debug, linker_language=language.cpp, rpath_lib_dir=None,
            rpath_bin_dir=None, version=None, default_libs=(), subsystem=None):
        raise NotImplementedError()

    def make_dependencies(self, source_files):
        raise NotImplementedError()

    def dynamic_library_extension(self):
        raise NotImplementedError()

    def executable_extension(self):
        raise NotImplementedError()

    def input_dynamic_library_default_extension(self):
        raise NotImplementedError()

    def static_library_default_extension(self):
        raise NotImplementedError()

    def _find_executables(self):
        self.executable_paths = dict(
            (program, self._find_program(program))
            for program in self._programs)

        missing_executables = [
            program for program, file_path in self.executable_paths.items()
            if file_path is None]
        if len(missing_executables) == 0:
            return

        raise RuntimeError(
            "The following programs could not be found in the path:" +
            " ".join(missing_executables))

    def _find_program(self, program):
        # TODO: By accessing $PATH through client_environ, we'll reload all
        #       the madefiles if $PATH changes.  However, we don't really want
        #       to do that -- we just want to reload them if it affects the
        #       programs used here.  Changing $PATH from madefiles themselves
        #       is reasonable to do, in fact, and would cause unnecessary
        #       reloading.
        for path_dir in runtime.client_environ["PATH"].split(_path_separator()):
            file_path = "%s/%s" % (path_dir, program)
            if os.path.isfile(file_path):
                return file_path

        return None

    def _filter_extra_options(
            self, extra_options_dict, op, is_debug=False,
            language=language.cpp):
        return extra_options_dict.get((op, is_debug, language), ())

    def d_files_from_object_files(self, object_files):
        if len(object_files) == 0:
            return []

        object_file_dir = os.path.dirname(object_files[0])

        return [
            misc.join_with_slashes(
                object_file_dir,
                ".%s.d" % os.path.splitext(os.path.basename(object_file))[0])
            for object_file in object_files]


class GCCCompilerChain(CPPCompilerChain):
    name = "gcc"
    _programs = ("gcc", "g++", "ar")
    _language_to_program = {language.cpp: "g++", language.c: "gcc"}
    default_optimization_options = ("-O3",)

    def dynamic_library_extension(self):
        return {"Darwin": ".dylib", "Linux": ".so"}[platform.system()]

    def executable_extension(self):
        return ""

    def input_dynamic_library_default_extension(self):
        return (".dylib" if platform.system() == "Darwin" else ".so")

    def static_library_default_extension(self):
        return ".a"

    def _find_program(self, program):
        # Allow the user to override the default programs using environment
        # variables.
        return CPPCompilerChain._find_program(
            self, runtime.client_environ.get(
                {"gcc": "CC", "g++": "CXX", "ar": "AR"}[program], program))

    def compile_files(
            self, source_and_object_files, defines, include_dirs, is_debug,
            is_for_dynamic, extra_options_dict={},
            overridden_debug_options=None,
            overridden_optimization_options=None):
        source_files, object_files = zip(*source_and_object_files)

        source_extension = misc.extension(source_files[0])
        assert(misc.extension(source_file) == source_extension
            for source_file in source_files)

        if source_extension not in (".c", ".cpp", ".C", ".cc", ".cxx", ".c++"):
            raise RuntimeError("Invalid source extension: " + source_extension)
        compiler_language = (
            language.c if source_extension == ".c" else language.cpp)

        parts = [
            self.executable_paths[self._language_to_program[compiler_language]]]
        if struct.calcsize("P") == 8:
            parts.append("-m64")

        if platform.system() != "Windows":
            parts.append("-fPIC")

        parts.append("-c")
        parts.extend(
            _not_none(overridden_debug_options, ("-g",)) if is_debug
            else _not_none(overridden_optimization_options,
                self.default_optimization_options))
        parts.extend(("-Wall", "-W"))
        parts.extend("-D%s" % define for define in defines)
        parts.extend("-I%s" % include_dir for include_dir in include_dirs)
        parts.extend(self._filter_extra_options(
            extra_options_dict, operation.compile, is_debug,
            compiler_language))
        parts.extend(source_files)

        # Output make-style dependencies into .d files.  If we're only
        # compiling one file at a time, we can specify the output files
        # explicitly, to prefix the .d file with "." and possibly move the
        # output to a different directory.
        d_files = self.d_files_from_object_files(object_files)
        parts.extend(("-MMD", "-MP"))
        if len(source_and_object_files) == 1:
            parts.extend(("-MF", d_files[0]))
            parts.extend(("-o", object_files[0]))

        commands = [command.Command(
            parts, description="compile " + " ".join(source_files))]

        if len(source_and_object_files) != 1:
            # We're compiling multiple files and we can't specify output files.
            # We need to manually rename the .d files, and possibly move the
            # .d and object files into the output directory.
            commands.extend(
                _rename_file_command(
                    os.path.splitext(os.path.basename(source_file))[0] + ".d",
                    d_file)
                for source_file, d_file in zip(source_files, d_files))

            output_dir = os.path.dirname(object_files[0])
            if len(output_dir) != 0:
                commands.extend(
                    _rename_file_command(
                        os.path.basename(object_file), object_file)
                    for object_file in object_files)

        return commands

    def link_static_library(
            self, object_files, output_library, extra_options_dict={}):
        parts = [
            self.executable_paths["ar"],
            "cruvs",
            output_library]
        parts.extend(object_files)
        parts.extend(self._filter_extra_options(
            extra_options_dict, operation.link_static))
        return [command.Command(parts, description="link " + output_library)]

    def link_dynamic_library(
            self, object_files, output_library, link_dirs, link_libraries,
            is_debug, import_lib=None,
            linker_language=language.cpp, use_rpath=True,
            version=None, default_libs=(),
            extra_options_dict={}):
        rpath_lib_dir = (None if use_rpath is None
            else os.path.dirname(os.path.abspath(output_library)))
        return self._get_link_command(object_files, output_library, link_dirs,
            link_libraries, is_debug=is_debug, create_library=True,
            linker_language=linker_language, rpath_bin_dir=None,
            rpath_lib_dir=rpath_lib_dir, extra_options_dict=extra_options_dict)

    def link_binary(
            self, object_files, output_binary, link_dirs, link_libraries,
            is_debug, linker_language=language.cpp,
            rpath_lib_dir=None, rpath_bin_dir=None, version=None,
            default_libs=(), subsystem=None, extra_options_dict={}):
        return self._get_link_command(object_files, output_binary, link_dirs,
            link_libraries, is_debug=is_debug, create_library=False,
            linker_language=linker_language, rpath_bin_dir=rpath_bin_dir,
            rpath_lib_dir=rpath_lib_dir, extra_options_dict=extra_options_dict)

    def _get_link_command(
            self, object_files, output, link_dirs, link_libraries, is_debug,
            create_library, linker_language, rpath_bin_dir, rpath_lib_dir,
            extra_options_dict):
        if not create_library and rpath_bin_dir is None:
            rpath_bin_dir = os.path.dirname(os.path.abspath(output))

        parts = [
            self.executable_paths[self._language_to_program[linker_language]],
            "-o", output,
            "-fmessage-length=0",
        ]

        parts.extend(("-g",) if is_debug else self.default_optimization_options)

        if create_library:
            parts.extend((
                "-shared",
                "-Wl,-soname,%s.so" % os.path.basename(
                    os.path.splitext(output)[0]),
                "-Wl,--no-undefined",
                "-Wl,-Bsymbolic-functions",
            ))

        if rpath_lib_dir is not None:
            parts.append("-Wl,-rpath,$ORIGIN/" + (
                "." if create_library
                else os.path.relpath(rpath_lib_dir, rpath_bin_dir)))

        for link_library in link_libraries:
            assert(link_library.startswith("lib"))

        link_dirs = list(link_dirs)
        parts.extend("-L%s" % link_dir for link_dir in link_dirs)
        parts.extend(object_files)
        parts.extend(
            "-l%s" % os.path.splitext(link_library[3:])[0]
            for link_library in link_libraries)
        parts.extend(self._filter_extra_options(
            extra_options_dict,
            (operation.link_dynamic if create_library
                else operation.link_binary),
            is_debug, linker_language))
        return [command.Command(parts, description="link " + output)]

    def make_dependencies(self, source_files, output_file, defines):
        # We build dependencies when we compile individual source files, so we
        # don't need to do anything here.
        pass

    _cached_gcc_versions = {}

    def gcc_version(self, is_cpp=True):
        gcc_path = self.executable_paths["g++" if is_cpp else "gcc"]
        gcc_version = self._cached_gcc_versions.get(gcc_path)
        if gcc_version is not None:
            return gcc_version

        gcc_version = self._determine_gcc_version(gcc_path)
        self._cached_gcc_versions[gcc_path] = gcc_version
        return gcc_version

    def _determine_gcc_version(self, gcc_path):
        version_output = subprocess.Popen(
            (gcc_path, "-v"), stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT).communicate()[0]
        version_string = re.search(
            "gcc version ([^ ]*)", version_output).groups()[0]
        return tuple(int(x) for x in version_string.split(".")[:2])

class MSVCCompilerChain(CPPCompilerChain):
    name = "msvc"
    # TODO: Also look for makedepend.
    _programs = ("cl", "lib", "link") # "makedepend"
    default_optimization_options = ("-O2",)

    def dynamic_library_extension(self):
        return ".dll"

    def executable_extension(self):
        return ".exe"

    def input_dynamic_library_default_extension(self):
        return ".lib"

    def static_library_default_extension(self):
        return ".lib"

    def __init__(self, msvc_version=None):
        self.msvc_dir = self._find_msvc_dir(msvc_version)
        if self.msvc_dir is None:
            self.msvc_bin_dir = None
        else:
            self.msvc_bin_dir = (
                "%s/bin/amd64" if struct.calcsize("P") == 8
                else "%s/bin") % self.msvc_dir
        CPPCompilerChain.__init__(self)

    def _find_msvc_dir(self, msvc_version=None):
        # TODO: We currently require $TOOLROOT to be set, but we can find
        #       the compiler path from the registry.
        return runtime.client_environ["TOOLROOT"]

    def _find_program(self, program):
        if self.msvc_bin_dir is not None:
            program_path = "%s/%s.exe" % (self.msvc_bin_dir, program)
            if os.path.isfile(program_path):
                return program_path

        result_path = CPPCompilerChain._find_program(self, program)
        return (_get_short_path_name(result_path)
            if result_path is not None else None)

    def compile_files(
            self, source_and_object_files, defines, include_dirs, is_debug,
            is_for_dynamic, extra_options_dict={},
            overridden_debug_options=None,
            overridden_optimization_options=None):
        source_files, object_files = zip(*source_and_object_files)
        source_extension = misc.extension(source_files[0])
        object_extension = misc.extension(object_files[0])
        assert(misc.extension(source_file) == source_extension and
                misc.extension(object_file) == object_extension
            for source_file, object_file in source_and_object_files)

        output_dir = os.path.dirname(object_files[0])
        # TODO: Assert that all object files are in the same output directory
        #       (or else add support to handle that case).

        compiler_language = (language.cpp if source_extension in (".cpp", ".C")
            else language.c)

        parts = [self.executable_paths["cl"]]
        if compiler_language == language.cpp:
            parts.append("-TP")

        # TODO: Ideally, we wouldn't show this flag in the output, since we're
        #       filtering its results and acting as though it's not turned on.
        parts.append("-showIncludes")

        parts.append("-MD" if is_for_dynamic else "-MT") # support threading
        parts.append("-c") # compile without linking
        parts.extend(
            _not_none(overridden_debug_options, ("-Od",)) if is_debug
            else _not_none(overridden_optimization_options,
                self.default_optimization_options))

        parts.extend("-D%s" % define for define in defines)
        parts.extend("-I%s" % include_dir for include_dir in include_dirs)
        parts.extend(self._filter_extra_options(
            extra_options_dict, operation.compile, is_debug, compiler_language))
        parts.extend(source_files)

        rename_object_files_from_obj = False
        if len(object_files) > 1:
            if len(output_dir) != 0:
                parts.append("-Fo%s/" % output_dir)
            rename_object_files_from_obj = object_extension != ".obj"
        else:
            parts.append("-Fo" + object_files[0])

        # Unfortunately, if we're compiling multiple files at once, there's
        # no way to know which file included which headers.  So, we err on
        # the safe side and make each file being compiled depend on the entire
        # set of headers that get output.
        d_files = self.d_files_from_object_files(object_files)
        commands = [command.Command(
            parts, description="compile " + " ".join(source_files),
            stdout_filter=_build_msvc_include_file_filter(
                zip(source_files, d_files), self.msvc_dir))]

        if rename_object_files_from_obj:
            for object_file in object_files:
                commands.append(_rename_file_command(
                    os.path.splitext(object_file)[0] + ".obj", object_file))

        return commands

    def link_static_library(
            self, object_files, output_library, extra_options_dict={}):
        parts = [
            self.executable_paths["lib"],
            "-nologo",
            "-ignore:4221",
            "-OUT:%s" % self._to_windows_path(output_library)]
        parts.extend(object_files)
        parts.extend(self._filter_extra_options(
            extra_options_dict, operation.link_static))
        return [command.Command(parts, description="link " + output_library)]

    def link_dynamic_library(
            self, object_files, output_library, link_dirs, link_libraries,
            is_debug, import_lib=None,
            linker_language=language.cpp, use_rpath=True,
            version=None, default_libs=(), extra_options_dict={}):
        return self._get_link_command(object_files, output_library, link_dirs,
            link_libraries, is_debug=is_debug, create_library=True,
            import_lib=import_lib, version=version, default_libs=default_libs,
            subsystem=None, extra_options_dict=extra_options_dict)

    def link_binary(
            self, object_files, output_binary, link_dirs, link_libraries,
            is_debug, linker_language=language.cpp,
            rpath_lib_dir=None, rpath_bin_dir=None, version=None,
            default_libs=(), subsystem=None, extra_options_dict={}):
        return self._get_link_command(object_files, output_binary, link_dirs,
            link_libraries, is_debug=is_debug, create_library=False,
            import_lib=None, version=version, default_libs=default_libs,
            subsystem=subsystem, extra_options_dict=extra_options_dict)

    def _get_link_command(self, object_files, output, link_dirs,
            link_libraries, is_debug, create_library, import_lib, version,
            default_libs, subsystem, extra_options_dict):
        # Note that object files may include a resource file.

        # TODO: Handle the is_debug parameter.

        parts = [
            self.executable_paths["link"],
            "-nologo",
            "-incremental:no",
            "-OUT:%s" % self._to_windows_path(output),
        ]

        if create_library:
            parts.append("-DLL")
            parts.append(
                "-IMPLIB:%s" % self._to_windows_path(import_lib))
        else:
            parts.append("-subsystem:%s" % _not_none(subsystem, "windows"))

            if struct.calcsize("P") == 8:
                parts.extend((
                    "-machine:X64",
                    "-largeaddressaware",
                ))

        link_dirs = list(link_dirs)
        link_dirs.extend(
            self._to_windows_path(self._to_windows_path(lib_dir))
                for lib_dir in self._default_lib_dirs())
        parts.extend("-LIBPATH:%s" % self._to_windows_path(link_dir)
            for link_dir in link_dirs)

        if version is not None:
            parts.append("-VERSION:%s" % version)

        parts.extend("-DEFAULTLIB:%s.LIB" % lib for lib in default_libs)
        parts.append("-DEFAULTLIB:DBGHELP.LIB")
        parts.append("-DELAYLOAD:DBGHELP.DLL")

        parts.extend(object_files)
        parts.extend(link_libraries)
        parts.extend(self._filter_extra_options(
            extra_options_dict,
            (operation.link_dynamic if create_library
                else operation.link_binary),
            is_debug))
        return [command.Command(parts, description="link " + output)]

    def make_dependencies(self, source_files, output_file, defines):
        parts = [self.executable_paths["makedepend"]]
        parts.extend(defines)
        parts.extend(("-f", output_file, "--"))
        parts.extend(source_files)
        return [command.Command(parts, stderr_filter=lambda line: "")]

    def _default_lib_dirs(self):
        subdirs = (
            ["lib/amd64", "PlatformSDK/lib/amd64"]
            if struct.calcsize("P") == 8
            else ["lib", "PlatformSDK/lib"])
        return ["%s/%s" % (self.msvc_dir, subdir)
            for subdir in subdirs]

    def _to_windows_path(self, path):
        # TODO: Do we need to map cygwin paths to Windows paths here (e.g.
        #       /home/foo to c:/cygwin/home/foo?)
        return path.replace("/", "\\")

# It would be nice if we could use a class here with a __call__ method,
# but unfortunately we need to use a function so it's easily marshallable.
def _build_msvc_include_file_filter(source_and_d_files, msvc_dir):
    # TODO: With gcc, the .d file contains dependencies from the .o, instead
    #       of from source files.  In either case, we ignore what comes
    #       before the :, though.  Note that we could get the .o extension
    #       via compilersettings._object_extension().

    source_and_d_files = source_and_d_files[:]
    header_files = []
    header_files_set = set()
    unix_path_msvc_dir = msvc_dir.replace("\\", "/")

    def stdout_filter(line):
        if len(line) == 0:
            # TODO: Properly handle files that contain spaces.  Does makedepend
            #       let you put quoted file names in the dependencies for a
            #       target?
            header_string = " ".join(header_files)
            for source_file, d_file in source_and_d_files:
                with open(d_file, "w") as open_file:
                    file_contents = (
                        source_file + ": " + header_string + "\n"
                        if len(header_files) != 0 else "")
                    open_file.write(file_contents)
            return ""

        prefix = "Note: including file:"
        if not line.startswith(prefix):
            return line

        header_file = line[len(prefix):].strip()
        if (not header_file.replace("\\", "/").startswith(unix_path_msvc_dir)
                and header_file not in header_files_set):
            header_files_set.add(header_file)
            header_files.append(header_file)

        return ""

    return stdout_filter

# TODO: Move this function to util.misc?
def _not_none(value, default_if_none):
    return (value if value is not None else default_if_none)

def _rename_file_command(from_file, to_file):
    return command.FunctionCall(
        "mv %s %s" % (from_file, to_file),
        __import__("os").rename, from_file, to_file)

def _path_separator():
    return (";" if platform.system() == "Windows" and
        sys.platform != "cygwin" else ":")

def _create_get_short_path_name():
    # TODO: This doesn't work -- it returns "[Error 2] The system cannot find
    #       the file specified."  Using it on a known path (such as Program
    #       Files) doesn't work, and using the A version instead of the W
    #       version also doesn't work.
    win_GetShortPathNameW = ctypes.WINFUNCTYPE(
            ctypes.wintypes.DWORD, # return value
            ctypes.wintypes.LPCWSTR, # long_path
            ctypes.wintypes.LPVOID, # short_path_buffer
            ctypes.wintypes.DWORD, # num_chars_in_buffer
        )(("GetShortPathNameW", ctypes.windll.kernel32))

    max_num_chars = 4096
    short_path_buffer = ctypes.create_unicode_buffer(max_num_chars)

    def _get_short_path_name(file_path):
        assert(file_path is not None)
        short_path_length = win_GetShortPathNameW(
            file_path.encode("utf-16"), short_path_buffer, max_num_chars)
        if short_path_length in (0, max_num_chars):
            raise ctypes.WinError()

        return short_path_buffer[:short_path_length].decode("utf-16")

    return _get_short_path_name

if platform.system() == "Windows":
    _get_short_path_name = _create_get_short_path_name()
else:
    def _get_short_path_name(file_path):
        return file_path

